/**
 * @fileOverview
 * Javascript tools to load and use GPS data in a web page. It loads tracks, 
 * waypoints and routes in maps using Mapstraction layer, draws altitude profiles
 * using Flotr charting library, reports derived data and allow user interaction 
 * between those objects.<br>
 * 
 * @author Javier Sanchez Portero
 * @version 0.1
 */
// License ////////////////////////////////////////////////////////////////////
// Copyright (C) 2009 Javier Sanchez Portero
//
// Licensed under the Apache License, Versioan 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
///////////////////////////////////////////////////////////////////////////////
//
// History:
//
///////////////////////////////////////////////////////////////////////////////

/** 
 * @name GPS
 * @namespace Holds all funcionality related to tools4gps.js library
 */
var GPS = GPS ? GPS : function() {

    // Private scope section 
    
    /**
     * Solve problem of typeof with null and arrays 
     * (http://javascript.crockford.com/remedial.html)
     * @private
     */
    function _typeOf(value) {
        var s = typeof value;
        if (s === 'object') {
            if (value) {
                if (value instanceof Array) {
                    s = 'array';
                }
            } else {
                s = 'null';
            }
        }
        return s;
    }

    /** 
     * Predefined locale settings
     * @see GPS.setI18N
     * @private
     */
    var _locales = {
      uk: {decsep: null,    dist: 'Km',    elev: 'm',    vel: 'Km/h',    time: 'h',
        labeldist: 'Dist.', labeltime: 'Time', labelelev: 'Elev.', labelvel: 'Vel.',
        distFactor: 1, elevFactor: 1, velFactor: 1, timeFactor: 1,
        tagbuttonzoomfalse: 'Select and zoom', 
        tagbuttonzoomtrue: 'Reset zoom',
        tagbuttonxaxisdist: 'Show time in \'X\' axis', 
        tagbuttonxaxishour: 'Show distance in \'X\' axis',
        tagbuttonyaxiselev: 'Show velocity in \'Y\' axis', 
        tagbuttonyaxisvel: 'Show elevation in \'Y\' axis',
        xaxistitledist: 'Distance',
        xaxistitletime: 'Time',
        yaxistitleelev: 'Elevation',
        yaxistitlevel: 'Velocity'
      },
      es: {decsep: ',',    dist: 'Km',    elev: 'm',    vel: 'Km/h',    time: 'h',
        labeldist: 'Dist.', labeltime: 'Tiempo', labelelev: 'Alt.', labelvel: 'Vel.',
        distFactor: 1, elevFactor: 1, velFactor: 1, timeFactor: 1,
        tagbuttonzoomfalse: 'Seleccionar y ampliar', 
        tagbuttonzoomtrue: 'Cancela ampliación',
        tagbuttonxaxisdist: 'Muestra el tiempo en el eje \'X\'', 
        tagbuttonxaxishour: 'Muestra la distancia en el eje \'X\'',
        tagbuttonyaxiselev: 'Muestra la velocidad en el eje \'Y\'', 
        tagbuttonyaxisvel: 'Muestra la altitud en el eje \'Y\'',
        xaxistitledist: 'Distancia',
        xaxistitletime: 'Tiempo',
        yaxistitleelev: 'Altitud',
        yaxistitlevel: 'Velocidad'
      }
    };

    /** 
     * Default locale setting
     * @private
     */
    var i18n = _locales.uk;

    /** 
     * Parses RFC 3339 date-time format
     * @param val RFC 3339 date-time string
     * @return {Date} A date
     * {@link http://www.ibm.com/developerworks/library/x-atom2json.html}
     * @private
     */
    function _parseData(val) {
        pattern = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?(?:[Tt](\d{2}):(\d{2}):(\d{2})(?:\.(\d*))?)?([Zz])?(?:([+-])(\d{2}):(\d{2}))?$/;
        if (!val) { 
            return null;
        }
        if (val instanceof Date) {
            return val; 
        }
        var m = pattern.exec(val); 
        if (!m) {
            return null;
        }
        var n = {year:new Number(m[1]), month: new Number(m[2]), day: new Number(m[3]), 
            hour: new Number(m[4]), minute: new Number(m[5]), second: new Number(m[6]),
            millis: new Number(m[7]?m[7]:0), gmt: m[8], dir: m[9], 
            offhour: new Number(m[10]?m[10]:0), offmin: new Number(m[11]?m[11]:0)}; 
        if (m.length > 8) { 
            var offset = ((n.offhour * 60) + n.offmin); 
            if (n.dir == "+") { 
                n.minute -= offset; 
            } else if (n.dir == "-") { 
                n.minute += offset; 
            }
        }
        return new Date(Date.UTC(n.year,n.month,n.day,n.hour,n.minute,n.second,n.millis));
    } 

    /** 
     * Create a _MedianFilter object
     * @class Class to filter an array with a median convolution
     * @param {uint} radius Radius of the filter
     * @private
     * @constructor
     */
    function _MedianFilter(radius) {
        this.size = 2 * radius + 1;
        var data = [];
        var results = [];
        var top = 0;
        
        // Sets the filter initial values
        data.push({val:-1e10, order:0});
        for (var i=1; i <= this.size+1; i++) {
            data.push({val:1e10, order:0});
        }
        
        /** 
         * Adds a value to the filter and push the result in a FIFO 
         * @private
         */
        this.push = function(v) {
            var temp;   var o = 0;
            if (top < this.size) {
                top++;
            }
            for (var i=1; i <= top; i++) {
                if (data[i].order == this.size) {
                    data.splice(i,1);
                    data.push({val:1e10, order:0});
                }
                if (data[i].val > v && data[i-1].val <= v) {
                    temp = v;   v = data[i].val;     data[i].val = temp;
                    temp = o;   o = data[i].order;   data[i].order = temp;
                }
                data[i].order++;
            }
            var j = Math.floor((top + 1) / 2);
            var median = data[j].val;
            if (top % 2 !== 0) {
                results.push(median);
            }
        };
        
        /**
         * Returns a value from the FIFO
         * @ private
         */
        this.shift = function() { 
            return results.shift(); 
        };
    }
    
    /**
     * Replaces decimal separator
     * @param {float} v A value
     * @param {uint} dec Fixed decimals
     * @return {string} Formatted value
     * @private
     */
    function _formatNumber(v, dec) {
        if (dec !== undefined) { 
            v = v.toFixed(dec);
        }
        return (i18n.decsep ? v.toString().replace('.', i18n.decsep) : v);
    }

    /**
     * Converts an hours value to hh:mm format 
     * @param {float} x Hour with decimals
     * @return {string} Formatted value
     * @private
     */
    function _hourToString(x) {
        var i = Math.floor(x);
        var j = Math.floor((x - i) * 60);
        return i + ':' + (j < 10 ? "0" + j : j);
    }

    /**
     * Static method to parse a point in a GPX file. 
     * @param pt 'wpt', 'rtept' or 'trkpt' element parsed from a GPX document
     * @return {GPS.Point} A GPS Point instance
     * @private
     */
    function _GPXparsePt(pt) {
        var lon = parseFloat(pt['-lon']);
        var lat = parseFloat(pt['-lat']);
        var point = new GPS.Point(lat, lon);
        if (pt.ele) {
            point.elev = parseFloat(pt.ele);
        }
        if (pt.time) {
            point.time = _parseData(pt.time);
        }
        if (pt.speed) {
            point.vel = _parseData(pt.speed) * 3.6; // from m/s to kmh
        }
        if (pt.name) {
            point.name = pt.name;
        }
        if (pt.desc) {
            point.html = pt.desc;
            if (pt.cmt && (pt.cmt != pt.desc)) {
                point.html += '<br/>' + pt.cmt;
            }
        }
        else if (pt.cmt) {
            point.html = pt.cmt;
        }
        return point;
    }

    /**
     * Static method to parse the traks in a GPX file. 
     * @param {GPS.Data} A GPS data set instance
     * @param traks parsed array of 'trk' elements from a GPX document
     * @private
     */
    function _GPXparseTrk(gpsData, traks) {
        // Return data from a GPX TrackPoint
        function parsePoint(trkpt) {
            var point = _GPXparsePt(trkpt);
            gpsData.addTrackpoint(point);
        }

        // Return data from a GPX track segment
        function parsePoints(trkseg) {
            gpsData.newSegment();
            if (_typeOf(trkseg.trkpt) == 'array') {
                trkseg.trkpt.each(parsePoint);
            } 
            else {
                parsePoint(trkseg.trkpt);
            }
        }

        // Parse data from diferent segments into gpsdata
        function parseSegments(trk) {
            if (_typeOf(trk.trkseg) == 'array') {
                trk.trkseg.each(parsePoints);
            } 
            else {
                parsePoints(trk.trkseg);
            }
        }
        if (_typeOf(traks) == 'array') {
            traks.each(parseSegments);
        } 
        else {
            parseSegments(traks);
        }
    }

    /**
     * Static method to parse the waypoints in a GPX file. 
     * @param {GPS.Data} A GPS data set instance
     * @param wpts parsed array of 'wpt' elements from a GPX document
     * @private
     */
    function _GPXparseWpt(gpsData, wpts) {

        // Return data from a GPX Point
        function parsePoint(wpt) {
            var point = _GPXparsePt(wpt);
            gpsData.addWaypoint(point);
        }
        if (_typeOf(wpts) == 'array') {
            wpts.each(parsePoint);
        } 
        else {
            parsePoint(wpts);
        }
    }

    /**
     * Static method to parse the routes in a GPX file. 
     * @param {GPS.Data} A GPS data set instance
     * @param routes array of 'rte' elements from a GPX document
     * @private
     */
    function _GPXparseRte(gpsData, routes) {

        // Return data from a GPX Point
        function parsePoint(rtept) {
            var point = _GPXparsePt(rtept);
            gpsData.addRoutepoint(point);
        }

        // Parse data from diferent waypoints into gpsdata
        function parseRoute(rte) {
            gpsData.newRoute();
            rte.rtept.each(parsePoint);
        }
        if (_typeOf(routes) == 'array') {
            routes.each(parseRoute);
        } 
        else {
            parseRoute(routes);
        }
    }

    /**
     * Adds user values 'opts' to default options 'dest' replacing existing ones.
     * @param {param: value} opts User options 
     * @param param: value} dest Destionation options
     * @private
     */
    function _setOptions(opts, dest) {
        dest = dest || this.options;
        opts = opts || {};
        for (var property in opts) {
            if (_typeOf(dest[property]) == "object" && opts[property]) {
                Object.extend(dest[property], opts[property]);
            }
            else {
                dest[property] = opts[property];
            }
        }
    }

    // Public scope section
    
    var GPS = {};
    
	GPS.version = '0.1';

    /**
     * Static method for setting the locale value for internationalization. Use it 
     * before any other.
     * @param {string, Object} loc Accepted values are 'uk' (default), 'es' or user definned like this:
     * <pre>{
     *    decsep: null,                          // decimal separator character
     *    dist: 'Km', elev: 'm',                 // profile ticks units
     *    vel: 'Km/h', time: 'h', 
     *    labeldist: 'Dist.', labeltime: 'Time', // profile tracking labels
     *    labelelev: 'Elev.', labelvel: 'Vel.',
     *    distFactor: 1, elevFactor: 1,          // profile values are multiplied by
     *    velFactor: 1, timeFactor: 1,           // this factors to unit conversion
     *    tagbuttonzoomfalse: 'Select and zoom', // titles of buttons
     *    tagbuttonzoomtrue: 'Reset zoom',
     *    tagbuttonxaxisdist: 'Show time in \'X\' axis', 
     *    tagbuttonxaxishour: 'Show distance in \'X\' axis',
     *    tagbuttonyaxiselev: 'Show velocity in \'Y\' axis',
     *    tagbuttonyaxisvel: 'Show elevation in \'Y\' axis'
     * }</pre>
     * For user defined buttons, the title is defined as 'tagbutton' + buttonName + buttonValue.
     * See {@link GPS.ButtonBar#_setOptions}
     */
    GPS.setI18N = function(loc) {
        i18n = loc ? (_locales[loc] || Object.extend(i18n, loc)) : locales.uk;
    };

    /**
     * @class Class that represents a track point, route point or waypoint data.
     * @memberOf GPS
     * @extends <a href='http://www.mapstraction.com/doc/LatLonPoint.html'>LatLonPoint</a>
     * @constructor 
     * @param {float} lat Latitude
     * @param {float} lon Longitude
     */
    GPS.Point = function(lat, lon) {
        /** Latitude 
         * @type float */
        this.lat = lat; 
        /** Longitude again 
         * @type float */
        this.lon = lon; 
        /** Longitude 
         * @type float */
        this.lng = lon; 
        /** Elevation in meters 
         * @type int */
        this.elev = null;
        /** Time (date-hour)
          * @type Date */
        this.time =  null;
        /** Horizontal distance (2D) in Km from origin of track 
         * @type float */
        this.dist = 0; 
        /** Spatial distance (3D) in Km from origin of track 
         * @type float */
        this.dist3d = 0; 
        /** Hours from origin of track 
         * @type float */
        this.hour = 0;
        /** Horizontal velocity (2D) in Km/s from last point 
         * @type float */
        this.vel = 0; 
        /** Spatial velocity (3D) in Km/s from last point
         * @type float */
        this.vel3d = 0;
        /** Name of the waypoint 
         * @type string */
        this.name = null;
        /** Description or comment of the waypoint
         * @type string */
        this.html = null;
    };
    GPS.Point.prototype = new LatLonPoint();

    /**
     * Create a GPS.Data object
     * @class Class that represents a GPS set of data (tracks, routes and waypoints).
     * @memberOf GPS
     * @constructor
     */
    GPS.Data = function() {
        /** Array of segments, wich are arrays of Track Points.
          * @type [[GPS.Point]] */
        this.trackpoints = [];
        /** Array of WayPoints 
         * @type [GPS.Points] */
        this.waypoints = [];
        /** Array of Routes, wich are arrays of Route points.
         * @type [[GPS.Points]] */
        this.routepoints = [];
        /** Total number of segments in the tracks
         * @type uint */
        this.numOfSegments = 0;
        /** Total number of points in the tracks
         * @type uint */
        this.numOfTrackpoints = 0;
        /** Total number of waypoints  
         * @type uint */
        this.numOfWaypoints = 0;
        /** Total number of routes  
         * @type uint */
        this.numOfRoutes = 0;
        /** Total number of route points in the routes
         * @type uint */
        this.numOfRoutepoints = 0;
        var lastPoint = null;
        var _this = this;
        
        /**
         * Gets the trackpoint at a index position as if all segments where joined.
         * @param {uint} n Index of the desired trackpoint.
         * @return {GPS.Point}
         */
        this.trackpointAt = function(n) {
            var tp = this.trackpoints;
            var i = 0;
            while (n - tp[i].length >= 0) {
                n -= tp[i].length;
                i++;
            }
            return tp[i][n];
        };
        
        /**
         * Inserts a new void segment in trackpoints
         */
        this.newSegment = function() {
            this.trackpoints.push([]);
            this.numOfSegments++;
            if (this.numOfTrackpoints === 0) {
                lastPoint = null;
            }
        };
        
        /**
         * Inserts a new void route in routepoints
         */
        this.newRoute = function() {
            this.routepoints.push([]);
            this.numOfRoutes++;
            lastPoint = null;
        };

        /**
         * Searchs for nearest point in the track given a key-value pair.
         * @param {float} val Value to search for.
         * @param {string} key Name of the field to search in.
         * @return {uint} Index position of the point as if all segments where joined.
         */
        this.indexOf = function(val, key) {
            if (!key) {
                key = 'dist';
            }
            var low = 0;
              var high = this.numOfTrackpoints - 1;
             while (low <= high) {
                 var mid = Math.floor((low + high) / 2);
                if (this.trackpointAt(mid)[key] === null) {
                    low++;
                }
                else {
                    if (this.trackpointAt(mid)[key] > val) {
                        high = mid - 1;
                    }
                    else if (this.trackpointAt(mid)[key] < val) {
                        low = mid + 1;
                    }
                    else {
                        return mid; // found
                    }
                }
             }
             return mid; // not found
        };
        
        /**
         * Calculates derived data for a point of a sequence
         * @param {GPS.Point} point Actual point in the sequence
         * @param {GPS.Point} lastPoint Prior point in the sequence
         * @private
         */
        function calcPoint(point, lastPoint) {
            if (!lastPoint) { 
                return;
            }
            var dist = point.distance(lastPoint);
            point.dist = lastPoint.dist + dist;
            var dist3d = dist;
            if (point.elev !== null && lastPoint.elev !== null) {
                var delev = (point.elev - lastPoint.elev) / 1000;
                dist3d = Math.sqrt(Math.pow(dist, 2) + Math.pow(delev, 2));
            }
            point.dist3d = lastPoint.dist3d + dist3d;
            if (point.time !== null && lastPoint.time !== null) {
                var dt = (point.time - lastPoint.time) / 3600000;
                var v = 0,  v3d = 0;
                if (dt !== 0) {
                    v = dist / dt;  
                    v3d = dist3d / dt; 
                }
                point.vel = point.vel || v;
                point.hour = lastPoint.hour + dt;
                point.vel3d = v3d;
            }
        }
        
        /**
         * Copy derived data from a point to another
         * @param {GPS.Point} to Target
         * @param {GPS.Point} from Source
         * @private
         */
        function copyPoint(to, from) {
            to.dist = from.dist;
            to.dist3d = from.dist3d;
            to.vel = from.vel;
            to.hour = from.hour;
            to.vel3d = from.vel3d;
        }
        
        /**
         * Returns a point in the track if another is at less than delta distance
         * @param {GPS.Point} point A point
         * @param {[GPS.Point]} track A track
         * @param {float} delta Minimun distance to the track
         * @return {GPS.Point} a point or null
         * @private
         */
        function isInTrack(point, delta) {
            var minDis = Number.MAX_VALUE;
            var near = null;
            for (var i=0; i < _this.numOfTrackpoints; i++) {
                var dist = point.distance(_this.trackpointAt(i));
                if (dist < minDis) {
                    near = _this.trackpointAt(i);
                    minDis = dist;
                }
            }
            return (minDis <= delta) ? near : null;
        }

        /**
         * Adds a point to the last segment and calculates derived data for it
         * (distance, velocity, hour).
         * @param {GPS.Point} trackpoint Point to be added.
         */
        this.addTrackpoint = function(trackpoint) {
            if (this.numOfSegments === 0) {
                this.newSegment();
            }
            calcPoint(trackpoint, lastPoint);
            this.trackpoints[this.numOfSegments - 1].push(trackpoint);
            this.numOfTrackpoints++;
            lastPoint = trackpoint;
        };

        /**
         * Adds a point to the list of waypoints and calculates derived data for it
         * (distance, velocity, hour).
         * @param {GPS.Point} waypoint Point to be added.
         */
        this.addWaypoint = function(waypoint) {
            var near = isInTrack(waypoint, 0.050);
            if (near) {
                copyPoint(waypoint, near);
            }
            else {
                waypoint.dist = null; 
                waypoint.dist3d = null; 
                waypoint.hour = null;
                waypoint.vel = null; 
                waypoint.vel3d = null;
            }
            this.waypoints.push(waypoint);
            this.numOfWaypoints++;
            lastPoint = waypoint;
        };

        /**
         * Adds a point to the last route and calculates derived data for it
         * (distance, velocity, hour).
         * @param {GPS.Point} waypoint Point to be added.
         */
        this.addRoutepoint = function(routepoint) {
            if (this.numOfRoutes === 0) {
                this.newRoute();
            }
            var near = isInTrack(routepoint, 0.050);
            if (near) {
                copyPoint(routepoint, near);
            }
            else {
                calcPoint(routepoint, lastPoint);
            }
            this.routepoints[this.numOfRoutes - 1].push(routepoint);
            this.numOfRoutepoints++;
            lastPoint = routepoint;
        };

    };

    /**
     * Create a GPS.Summary object
     * @param {GPS.Data} data Object that contents the track
     * @param {Object} opts User defined options (see {@link GPS.Summary#options})
     * @class Class that represents a set of global properties derived from a track
     */
    GPS.Summary = function(data, opts) {
        /** GPS data set
         * @type GPS.Data */
        this.data = data;
        /** 
         * User defined options.
         * Default values:
         * <pre>{ filterRadius: 2, stoppedDist: 0.001, stoppedVel: 0.5, minSlope: 0.05 }</pre>
         * Properties:
         * <pre>{uint} filterRadius - In order to avoid GPS acquisition noise, elevation is 
         *     median filtered with it 'filterRadius' neighbours before it calculates 
         *     elevation gain/loss. 0 = no filter.
         * {float} stoppedDist - Movements lower than this spatial distance (in meters) are ignored.
         * {float} stoppedVel  - Movements lower than this spatial velocity (in Km/h) are ignored.
         * {float} minSlope    - Below this slope (percent) is considered as flat.</pre>
         * @type Object
         */
        this.options = {
            filterRadius: 2,
            stoppedDist: 1/1000,
            stoppedVel: 0.5,
            minSlope: 0.05
        };

        /**
         * Adds user values to default options replacing existing ones.
         * @param opts User options 
         * @see GPS.Summary#options
         */
        this.setOptions = function(opts) {
            _setOptions(opts, this.options);
        };

        /**
         * Sets derived data to it initial values (0 or not calcuated)
         */
        this.reset = function() {
            /** Total horizontal (2D) distance in Km 
             * @type float */
            this.totDist = 0;
            /** Total spatial distance (3D) in Km 
             * @type float */
            this.totDist3d = 0;
            /** Minimum elevation in meters 
             * @type float */
            this.minElev = null;
            /** Maximum elevation in meters
             * @type float */
            this.maxElev = null;
            /** Elevation Gain in meters 
             * @type float */
            this.elevGain = null;
            /** Elevation Loss in meters
             * @type float */
            this.elevLoss = null;
            /** Maximum velocity in Km/s
             * @type float */
            this.maxVel = null;
            /** Average velocity in Km/s
             * @type float */
            this.avgVel = null;
            /** Total time in hours 
             * @type float */
            this.totTime = null;
            /** Total time stopped in hours 
             * @type float */
            this.totTimeStop = null;
            /** Total time going up in hours
             * @type float */
            this.totTimeUp = null;
            /** Total time going down in hours 
             * @type float */
            this.totTimeDown = null;
            /** Total time going in flat in hours 
             * @type float */
            this.totTimeFlat = null;
            /** Total time in movement in hours 
             * @type float */
            this.totTimeMove = null;
        };

        /**
         * Calculates acummulated data for a range of the track.
         * All tracks and segments are considered as a unique joined track.
         * @param from Initial index value of the range in this.data
         * @param to Last index value of the range in this.data
         */
        this.calculate = function(from, to) {
            var fradius = this.options.filterRadius;
            if (this.data.numOfTrackpoints < fradius) {
                return;
            }
            this.reset();
            if (!from) {
                from = 0;
            }
            if (!to && to !== 0) {
                to = this.data.numOfTrackpoints - 1;
            }
            var totv = 0, numv = 0;
            var filter = new _MedianFilter(fradius);
            lastPoint = this.data.trackpointAt(from);
            var lastelev = lastPoint.elev;
            var i = 0;
            for (i = 0; i <= fradius; i++) {
                filter.push(this.data.trackpointAt(from+i).elev);
            }
            filter.shift();
            // for each point
            for (i = from + 1; i <= to; i++) {
                var point = this.data.trackpointAt(i);
                this.totDist += (point.dist - lastPoint.dist);
                this.totDist3d += (point.dist3d - lastPoint.dist3d);
                var delev = 0;
                var elev = point.elev || 0;
                if (elev !== null) {
                    // Elevation is median filtered before it calculates elevation gain/loss
                    if (i < to - fradius + 1) {
                        filter.push(this.data.trackpointAt(i+fradius).elev);
                        elev = filter.shift();
                    }
                    if ((this.maxElev === null) || (elev > this.maxElev)) {
                        this.maxElev = elev;
                    }
                    if ((this.minElev === null) || (elev < this.minElev)) {
                        this.minElev = elev;
                    }
                    delev = elev - lastelev;
                    this.elevGain += ((delev > 0) ? delev : 0);
                    this.elevLoss += ((delev < 0) ? delev : 0);
                }
                if (point.time !== null) {
                    this.totTime += (point.hour - lastPoint.hour);
                    var dt = point.hour - lastPoint.hour;
                    // Points are not considered when there isn't movement
                    var stopped = (point.dist3d - lastPoint.dist3d < this.options.stoppedDist);
                    stopped = stopped || (point.vel3d < this.options.stoppedVel);
                    if (stopped) {
                        this.totTimeStop += dt;
                    }
                    else {
                        this.totTimeMove += dt;
                        // Calculates slope to distinguish flat values
                        var slope = Math.abs(delev) / 1000;
                        slope /= (point.dist - lastPoint.dist);
                        if (slope < this.options.minSlope) {
                            delev = 0;
                        }
                        if (delev > 0) {
                            this.totTimeUp += dt;
                        }
                        else if (delev < 0) {
                            this.totTimeDown += dt;
                        }
                        else {
                            this.totTimeFlat += dt;
                        }
                        if (point.vel !== null) {
                            totv += point.vel;
                            numv += 1;
                            if ((this.maxVel === null) || (point.vel > this.maxVel)) {
                                this.maxVel = point.vel;
                            }
                        }
                    }
                }
                lastPoint = point;
                lastelev = elev;
            }
            if (numv !== 0) {
                this.avgVel = totv / numv;
            }
        };

        // Constructor
        this.setOptions(opts);
    };

    /**
     * Create a GPS.Parser object
     * @param {string} url Source of data
     * @param {Object} opts User defined options (see {@link GPS.Parser#options})
     * @class Class to parse a source of GPX data into a GPS.Data object.
     */
    GPS.Parser = function(url, opts) {
        this.url = url;
        var _this = this;
        var gpsData = new GPS.Data();
    
        /** 
         * User defined options.
         * Default values:
         * <pre>{ trk: true, wpt: true, rte: true }</pre>
         * Properties:
         * <pre>{boolean} trk - If false, ignores tracks
         * {boolean} wpt - If false, ignores waypoints
         * {boolean} rte - If false, ignores routes</pre>
         * @type Object
         */
        this.options = {
            trk: true, wpt: true, rte: true
        };

        /**
         * Adds user values to default options replacing existing ones. See 
         * @param opts User options 
         * @see GPS.Parser#options
         */
        this.setOptions = function(opts) {
            _setOptions(opts, this.options);
        };

        /**
         * Does an asynchronous ajax request for data. On success, parses 
         * them and calls to onSuccessAction.<br/>
         * Example:
         * @example
         * // Containers
         * &lt;div id="map" style="width: 650px; height: 340px"&gt;&lt;/div&gt;
         * &lt;div id="buttons"&gt;
         *     &lt;img id="zoom" src="img/zoom_off.png" width="22" height="22"&gt;
         *     &lt;img id="xaxis" src="img/dist_off.png" width="22" height="22"&gt;
         *     &lt;img id="yaxis" src="img/elev_off.png" width="22" height="22"&gt;
         * &lt;/div&gt;
         * &lt;div id="perfil" style="width:650px; height:160px;"&gt;&lt;/div&gt;
         * &lt;div id="report"&gt;
         * Total distance: &lt;span id='totDist'&gt;&lt;/span&gt; Km&lt;br/&gt;
         * Total elevation gain: &lt;span id='elevGain'&gt;&lt;/span&gt; m&lt;br/&gt;
         * Total elevation loss: &lt;span id='elevLoss'&gt;&lt;/span&gt; m
         * &lt;/div&gt;
         * &lt;script type="text/javascript"&gt;
         *     // Wait till DOM's finished loading.
         *     document.observe('dom:loaded', function() {
         *         var map = new Mapstraction('map','google');
         *         map.addControls({ pan: true, zoom: 'small', map_type: true });
         *         var parser = new GPS.Parser("a_file.gpx");
         *         parser.run(function (gpsData) {
         *             var chart = new ("profile", gpsData);
         *             var report = new GPS.Report("report", gpsData, {'chart': chart});
         *             var buttons = new GPS.ButtonBar("buttons", {'chart': chart, 'report': report});
         *             var gpsmap = new GPS.Map(map, gpsData, {"chart": chart});
         *         });
         *     });            
         * &lt;/script&gt;
         * @param {function (data)} onSuccessAction User definned function that runs 
         * after successfull parsing. A {GPS.Data} data parameter is passed with the parsed data.
         */
        this.run = function(onSuccessAction) {
            new Ajax.Request(this.url, {
                method: 'post',
                onSuccess: function(request) {  
                    var xotree = new XML.ObjTree();
                    var json = xotree.parseXML( request.responseText );
                    if (json.parsererror) {
                        alert(json.parsererror['#text']);
                    }
                    else if (json.gpx) {
                        if (_this.options.trk && json.gpx.trk) {
                            _GPXparseTrk(gpsData, json.gpx.trk);
                        }
                        if (_this.options.wpt && json.gpx.wpt) {
                            _GPXparseWpt(gpsData, json.gpx.wpt);
                        }
                        if (_this.options.rte && json.gpx.rte) {
                            _GPXparseRte(gpsData, json.gpx.rte);
                        }
                        if (onSuccessAction) {
                            onSuccessAction(gpsData);
                        }
                    }
                },
                onException: function(request, exception) { 
                    alert("Exception " + exception.name + ": " + exception.message); 
                }
            });
        };

        // Constructor
        this.setOptions(opts);
    };
    
    /**
     * Create a GPS.Map object 
     * @param {mapstraction.map} map A Mapstraction map object
     * @param {GPS.Data} gpsData GPS.Data object to be loaded in the map
     * @param {Object} opts User defined options (see {@link GPS.Map#options}
     * @class Class to show GPS data in a map and control related events
     */
    GPS.Map = function(map, gpsData, opts) {
        /** Mapstraction map object
         * @type Mapstraction.map */
        this.map = map;
        /** GPS data set
         * @type GPS.Data */
        this.gpsData = gpsData;
        /** 
         * User defined options.
         * Default values:
         * <pre>{
         *     trk: {width: 3, color: '#ff0000', opacity: 1},
         *     rte: {width: 3, color: '#00ff00', opacity: 1},
         *     wpt: {},
         *     join: true, 
         *     info: true,
         *     chart: false,
         *     marker: {
		 *         icon: "img/icon52.png", iconSize: [32,32], iconAnchor: [16,16], 
		 *         iconShadow: "img/void.png", iconShadowSize: [32,32]
		 *     },
         *     followMarker: true
         * }</pre>
         * Properties:
         * <pre>{boolean | object} trk - If false, don't draw tracks. If object, set
         *     how to draw the tracks using mapstraction <a href='http://www.mapstraction.com/doc/Polyline.html'>Polyline</a> options:
         *     {uint} width - Track/route width
         *     {string} color - Track/route color in the form #RRGGBB
         *     {float} opacity - Track/route opacity between 0.0 and 1.0
         * {boolean | object} rte - If false, don't draw routes. If object, set
         * how to draw the routes using mapstraction Polyline options (see trk).
         * {boolean | object} wpt - If false, don't draw waypoints. If object, set
         *     how to draw the waypoints using mapstraction <a href='http://www.mapstraction.com/doc/Marker.html'>Marker</a> options:
         *     {string} icon - Url of the image to be used as icon
         *     {[uint, uint]} iconSize - Array size in pixels of the icon image
         *     {[uint, uint]} iconAnchor - Array offset of the anchor point
         *     {string} iconShadow - Url of the image to be used as shadow of the icon
         *     {[uint, uint]} iconShadowSize - Array size in pixels of the shadow image
         * {boolean} join - If true, join all the segments, tracks or routes
         * {boolean | string} info - Set how is showed the waypoints info. None 
         *     if false, in a bubble popup if true or in a DOM element identified by it id.
         * {GPS.Profile} chart - A Profile object to link with (a marker in the map 
         *     follows mouse movement in the profile) or false
         * {object} marker - Set the options of the marker linked to a chart
         *     using mapstraction Marker options (see wpt). 
         * {boolean}followMarker - If true, the maps pans to the point 
         *     corresponding the mouse movement in the profile.</pre>
         * @type Object
         */
        this.options = {
            trk: {width: 3, color: '#ff0000', opacity: 1}, 
            rte: {width: 3, color: '#00ff00', opacity: 1}, 
            wpt: {}, 
            marker: {
		        icon: "img/icon52.png", iconSize: [32,32], iconAnchor: [16,16], 
		        iconShadow: "img/void.png", iconShadowSize: [32,32]
		    },
            join: true, info: true, chart: false, followMarker: true
        };

        var _this = this;
        var marker;

        /**
         * Adds user values to default options replacing existing ones.
         * @param {Object} opts User options 
         * @see GPS.Map#options
         */
        this.setOptions = function(opts) {
            _setOptions(opts, this.options);
        };

        /**
         * Shows tracks, routes and waypoints in the map.
         * @param {Object} opts User defined options
         * @see GPS.Map#options
         */
        this.draw = function(opts) {
            var o = Object.clone(this.options);
            Object.extend(o, opts || {});
            var i = 0;
            var j = 0;
            var tp = [[]];
            var myPoly = null;
            if (o.trk) {
                if (o.join) {
                    for (i=0; i < this.gpsData.numOfSegments; i++) {
                        for (j=0; j < this.gpsData.trackpoints[i].length; j++) {
                            tp[0].push(this.gpsData.trackpoints[i][j]);
                        }
                    }
                }
                else {
                    tp = this.gpsData.trackpoints;
                }
                for (i=0; i < tp.length; i++) {
                    if (tp[i].length) {
                        myPoly = new Polyline(tp[i]);
                        this.map.addPolylineWithData(myPoly, o.trk);
                    }
                }
            }
            tp = [[]];
            if (o.rte) {
                if (o.join) {
                    for (i=0; i < this.gpsData.numOfRoutes; i++) {
                        for (j=0; j < this.gpsData.routepoints[i].length; j++) {
                            tp[0].push(this.gpsData.routepoints[i][j]);
                        }
                    }
                }
                else {
                    tp = this.gpsData.routepoints;
                }
                for (i=0; i < tp.length; i++) {
                    if (tp[i].length) {
                        myPoly = new Polyline(tp[i]);
                        this.map.addPolylineWithData(myPoly, o.rte);
                    }
                }
            }
            if (o.wpt) {
                for (i=0; i < this.gpsData.numOfWaypoints; i++) {
                    var p = this.gpsData.waypoints[i];
                    var marker = new Marker(p);
                    if (p.name) {
                        marker.setLabel(p.name);
                    }
                    if (p.html) {
                        if (_typeOf(o.info) == "string") {
                            marker.setInfoDiv(p.html, o.info);
                        }
                        else if (o.info) {
                            marker.setInfoBubble(p.html);
                        }
                    }
                    this.map.addMarkerWithData(marker, o.wpt);
                }
            this.map.autoCenterAndZoom(); 
            }
        };

        // Constructor
        this.setOptions(opts);
        var chart = this.options.chart;
        // Hooks to the chart profile mousemove event to redraw a marker in the map or pan it.
        if (chart) {
            chart.container.observe('flotr:mousemove', function(event) {
                var position = event.memo[1];
                var i = _this.gpsData.indexOf(position.x, _this.options.chart.options.x);
                if (marker) {
                    _this.map.removeMarker(marker);
                }
                marker = new Marker(_this.gpsData.trackpointAt(i));
                _this.map.addMarkerWithData(marker, _this.options.marker);
                if (_this.options.followMarker) {
                    // If the api is google, pan looks better than center (the track don't wanish).
                    if (_this.map.api == 'google') {
                        _this.map.getMap().panTo(marker.location.toGoogle());
                    }
                    else {
                        _this.map.setCenter(marker.location);
                    }
                }
            });
        }
        this.draw();
        return this;
    };

    /**
     * Create a GPS.Profile object
     * @param {string} idprofile Id of the profile DOM element
     * @param {GPS.Data} gpsData Source of data
     * @param {para: value...} opts User defined options. See {@link GPS.Profile#options}
     * @class Class to show diferent profiles (elevation, velocity) in a
     * <a href='http://solutoire.com/flotr'>Flotr</a> chart.
     */
    GPS.Profile = function(idprofile, gpsData, opts) {
        /** DOM node than contains the Flotr chart 
         * @type node */
        this.container = $(idprofile);
        /** GPS data set
         * @type GPS.Data */
        this.gpsData = gpsData;
        /** 
         * User defined options.
         * Default values:
         * <pre>{ 
         *     x: 'dist', 
         *     y: 'elev', 
         *     join: true, 
         *     trackingEnabled: true, 
         *     zoomEnabled: false,
         *     xFactor: 1, 
         *     yFactor: 1, 
         *     xUnit: false, 
         *     yUnit: false,
         *     trk: true, 
         *     wpt: true, 
         *     rte: true
         * }</pre>
         * Properties:
         * <pre>{string} x: this.gpsData property to be used in the x axis. Admited 
         *     values are 'dist', 'dist3d' or 'hour'
         * {string} y: this.gpsData property to be used in the y axis. Admited 
         *     values are 'elev', 'vel' or 'vel3d'
         * {boolean} join: if true, join the diferent segments and tracks
         * {boolean} trackingEnabled: If true, enable mouse tracking 
         *     (shows x-y values in one corner of the chart)
         * {boolean} zoomEnabled: If true, enable mouse selection and zoom
         * {float} xFactor: X values are multiplied by this factor to unit conversion, 
         *     if not defined, use i18n factors (see {@link GPS.setI18N})
         * {float} yFactor: Y values are multiplied by this factor to unit conversion, 
         *     if not defined, use i18n factors (see {@link GPS.setI18N})
         * {string} xUnit: Put a string to show X axis ticks units, if not defined, 
         *     show i18n units (see {@link GPS.setI18N})
         * {string} yUnit: Put a string to show Y axis ticks units, if not defined, 
         *     show i18n units (see {@link GPS.setI18N})
         * {boolean} trk - If false, don't show tracks, if object, 
         *     they are drawed with these options. Use properties for a plot serie as defined in
         *     <a href='http://solutoire.com/flotr/docs/'>Plotr documentation</a>
         * {boolean | object} wpt - If false, don't show waypoints, if object, 
         *     they are drawed with these options. Use properties for a plot serie as defined in
         *     <a href='http://solutoire.com/flotr/docs/'>Plotr documentation</a>
         * {boolean | object} rte - If false, don't show routes, if object, 
         *     they are drawed with these options. Use properties for a plot serie as defined in
         *     <a href='http://solutoire.com/flotr/docs/'>Plotr documentation</a></pre>
         * You can use global properties as defined in <a href='http://solutoire.com/flotr/docs/'>Plotr documentation</a>
         * for all the series (including the tracks).
         * @type Object
         */
        this.options = {
            x: 'dist', y: 'elev', 
            join: true,
            trackingEnabled: true,
            zoomEnabled: false,
            xFactor: 1, yFactor: 1,  // unit conversion
            xUnit: false, yUnit: false,
            xaxis: {}, yaxis: {},
            HtmlText: false,
            trk: true,
            wpt: true,
            rte: true
        };

        // Array of segments wich are arrays of [x,y] pairs extracted from 
        // the selected properties in this.gpsData. See x and y properties in GPS.Profile#options
        var profileData = [];
        // Units labels
        var units = {dist: i18n.dist, dist3d: i18n.dist, hour: i18n.time, 
            elev: i18n.elev, vel: i18n.vel, vel3d: i18n.vel};
        // Labels for x-y values
        var labels = {dist: i18n.labeldist, dist3d: i18n.labeldist, hour: i18n.labeltime, 
            elev: i18n.labelelev, vel: i18n.labelvel, vel3d: i18n.labelvel};
        // Units conversion factors
        var factors = { dist: i18n.distFactor, dist3d: i18n.distFactor, hour: i18n.timeFactor, 
            elev: i18n.elevFactor, vel: i18n.velFactor, vel3dFactor: i18n.velFactor };
        // Minimun diference to draw a point.
        var delta = {dist: 0.02, dist3d: 0.02, hour: 20/3600, elev: 0, vel: 0, vel3d: 0};
        // Minimun allowed zoom
        var minzoom = {dist: 0.1, dist3d: 0.1, hour: 0.1, elev: 10, vel: 1, vel3d: 1};
        // Private scope access to this.
        var _this = this;
        
        /**
         * Adds user values in opts to dest replacing existing ones.
         * @param {param: value} opts User options 
         * @param param: value} dest Destionation options
         * @see GPS.Profile#options
         */
        this.setOptions = function(opts, dest) {
            Object.extend(dest || this.options, opts || {});
            _enableTracking(this.options);
            _enableZoom(this.options);
        };

        // Activates zoom
        function _enableZoom(o) {
            if (o.zoomEnabled) {
                Object.extend(o, {selection: {mode: 'xy'}});
                // Hook into the 'flotr:select' event.
                _this.container.observe('flotr:select', function(evt) {
                    // draw a new graph with bounded axis. The axis correspond to the selection just made.
                    var area = evt.memo[0];
                    var xaxis = Object.clone(_this.options.xaxis);
                    var yaxis = Object.clone(_this.options.yaxis);
                    var x = _this.options.x;
                    var y = _this.options.y;
                    Object.extend(xaxis, (area.x2 - area.x1 > minzoom[x]) ? 
                        {min:area.x1, max:area.x2} : 
                        {min:(area.x1 + area.x2 - minzoom[x]) / 2, max: (area.x1 + area.x2 + minzoom[x]) / 2});
                    Object.extend(yaxis, (area.y2 - area.y1 > minzoom[y]) ?
                        {min:area.y1, max:area.y2} :
                        {min:(area.y1 + area.y2 - minzoom[y]) / 2, max: (area.y1 + area.y2 + minzoom[y]) / 2});
                    _this.draw({'xaxis': xaxis, 'yaxis': yaxis});
                });
            }
            else {
                Object.extend(o, {selection: {}});
            }
        }
        
        // Activates mouse tracking
        function _enableTracking(o) {
            if (o.trackingEnabled) {
                Object.extend(o, {mouse: {
                    track: true, trackMode: 'x', lineColor: 'purple',
                    sensibility: 1, trackDecimals: 2,
                    trackFormatter: function(obj){ return labels[_this.options.x] + ' = ' +
                        (_this.options.x == 'hour' ? _hourToString(obj.x) : _formatNumber(obj.x)) + ' ' +
                        labels[_this.options.y] + ' = ' +
                        (_this.options.y == 'hour' ? _hourToString(obj.y) : _formatNumber(obj.y)); }
                }});
            }
            else {
                Object.extend(o, {mouse: {}});
            }
        }

        /** 
        * Used by this.loadData to load tracks and routes
        */
        function _loadProfile(tp, o, topts) {
            var xFactor = o.xFactor || factors[o.x];
            var yFactor = o.yFactor || factors[o.y];
            var lastpoint = tp[0][0];
            var pointArray = {data:[]};
			if (!topts.color && !o.colors) {
				if (_typeOf(topts) != 'object') {
                    topts = {};
                }
				Object.extend(topts, {color: (o.x.substr(0,4) == 'dist' ? '#4da74d' : '#00A8F0')});
			}
            Object.extend(pointArray, topts);
            // for each segment
            for (var i=0; i < tp.length; i++) {
                if (!o.join) {
                    pointArray = {data:[]};
                    Object.extend(pointArray, topts);
                }
                for (var j=0; j < tp[i].length; j++) {
                    if ((i+j === 0) || (tp[i][j][o.x] - lastpoint[o.x] > o.minDeltaX)) {
                        pointArray.data.push([tp[i][j][o.x] * xFactor,  tp[i][j][o.y] * yFactor]);
                        lastpoint = tp[i][j];
                    }
                }
                if (!o.join) {
                    profileData.push(pointArray);
                }
            }
            if (o.join) {
                profileData.push(pointArray);
            }
        }
        
        /**
         * Load selected profile properties from gpsdata. 
         * See x and y properties in {@link GPS.Profile#options}
         */
        this.loadData = function() {
            var o = _this.options;
            var xFactor = o.xFactor || factors[o.x];
            var yFactor = o.yFactor || factors[o.y];
            o.minDeltaX = o.minDeltaX || delta[o.x];  // it filters x values
            profileData = [];
			var pc = o.wpt.color || (o.x.substr(0,4) == 'dist' ? '#4da74d' : '#00A8F0');
            // load tracks
            if (o.trk && this.gpsData.numOfTrackpoints > 0) {
                _loadProfile(this.gpsData.trackpoints, o, o.trk);
            }
            // load routes
            if (o.rte && this.gpsData.numOfRoutes > 0) {
                _loadProfile(this.gpsData.routepoints, o, o.rte);
            }
            // load waypoints
            if (o.wpt && this.gpsData.numOfWaypoints) {
                var waypoints = {data: [], color: pc, points: {show: true, fill: true, fillColor: pc}};
                this.setOptions(o.wpt, waypoints);
                var wp = this.gpsData.waypoints;
                for (var i=0; i < wp.length; i++) {
                    waypoints.data.push([wp[i][o.x] * xFactor,  wp[i][o.y] * yFactor]);
                }
                profileData.push(waypoints);
            }
        };

        /**
         * Shows the profile.
         * @param {param: value} opts User defined options 
         * @see GPS.Profile#options
         */
        this.draw = function(opts) {
            opts = opts || {};
            opts.xaxis = opts.xaxis || {};
            opts.yaxis = opts.yaxis || {};
            // Set options that change with axis selection
            opts.xaxis.tickFormatter = opts.xaxis.tickFormatter ||
                function(n) { return (_this.options.x == 'hour' ? _hourToString(n) : _formatNumber(n)) +
                        ' ' + (_this.options.xUnit || units[_this.options.x]);};
            opts.yaxis.tickFormatter = opts.yaxis.tickFormatter ||
                function(n){ return (_this.options.y == 'hour' ? _hourToString(n) : _formatNumber(n)) +
                    ' ' + (_this.options.yUnit || units[_this.options.y]);};
            opts.xaxis.title = (_this.options.x == 'hour' ? i18n.xaxistitletime : i18n.xaxistitledist);
            opts.yaxis.title = (_this.options.y == 'elev' ? i18n.yaxistitleelev : i18n.yaxistitlevel);
            opts.minDeltaX = opts.minDeltaX || delta[this.options.x];
            // Clone the options, so the 'options' variable always keeps intact, and adds user opts.
            var o = Object.clone(this.options);
            this.setOptions(opts, o);
            Flotr.draw(this.container, profileData, o);
        };
        
        // Constructor
        this.setOptions(opts);
        this.loadData();
        this.draw();
    };
  
    /**
     * Create a GPS.Report object
     * @param {string} idprofile Id of the report DOM element
     * @param {GPS.Data} gpsData Source of data
     * @param {Object} opts User defined options. See {@link GPS.Report#options}
     * @class Class to show a report with global properties derived from a track
     * @see GPS.Summary
     */
    GPS.Report = function(idreport, gpsData, opts) {
        /** DOM node than contains the report. See {@link GPS.Report#draw}
         * @type node */
        this.container = $(idreport);
        /** 
         * User defined options.
         * Default values: 
         * <pre>{
         *    decimals: {totDist: 2, 'totDist3d': 2, maxElev: 0, minElev: 0, 
         *        elevGain: 0, elevLoss: 0, avgVel: 2, maxVel: 2, totTime: 2, 
         *        totTimeStop: 2, totTimeUp: 2, totTimeFlat: 2, totTimeDown: 2, 
         *        totTimeMove: 2},
         *    chart: false
         * }</pre>
         * Properties:
         * <pre>{object} decimals - a list of number of fixed decimals for each available property
         * {GPS.Profile} chart - A Profile object to link with (recalculate summary and redraw
         *     on zoom event ) or false. </pre>
         * You can use also any summary option (see {@link GPS.Summary#options})
         * @type Object
         */
        this.options = {
            decimals: {totDist: 2, 'totDist3d': 2, maxElev: 0, minElev: 0, 
                elevGain: 0, elevLoss: 0, avgVel: 2, maxVel: 2, totTime: 2, 
                totTimeStop: 2, totTimeUp: 2, totTimeFlat: 2, totTimeDown: 2, 
                totTimeMove: 2},
            chart: false
        };
        /** Summary calculated from gpsData
         * @type GPS.Summary */
        this.summary = new GPS.Summary(gpsData, opts);
        var _this = this;

        /**
         * Adds user values to default options replacing existing ones.
         * @param {Object} opts User options 
         * @see GPS.Map#options
         */
        this.setOptions = function(opts) {
            _setOptions(opts, this.options);
        };

        /**
         * Dumps report values in the &lt;span&gt; elements contained into
         * this.container. The &lt;span&gt; id must coincide with the desired 
         * this.summary field. See {@link GPS.Summary}<br/>
         * Example:
         * @example
         * &lt;div id="report"&gt;
         * Total distance: &lt;span id='totDist'&gt;&lt;/span&gt; Km&lt;br/&gt;
         * Total elevation gain: &lt;span id='elevGain'&gt;&lt;/span&gt; m&lt;br/&gt;
         * Total elevation loss: &lt;span id='elevLoss'&gt;&lt;/span&gt; m
         * &lt;/div&gt;
         * @param from Initial index value of the range in this.data
         * @param to Last index value of the range in this.data
         */
        this.draw = function(from, to) {
            var g = this.summary;
            var s = this.container.getElementsByTagName('span');
            var decimals = this.options.decimals;
            _this.summary.calculate(from, to);
            for (var i=0; i < s.length; i++) {
                var id = s[i].id;
                if (g[id] !== undefined  && g[id] !== null) {
                    var v = (id.substr(0, 7) == 'totTime') ? _hourToString(g[id]) : _formatNumber(g[id], decimals[id] );
                    s[i].innerHTML = v;
                }
            }
        };
        
        // Constructor
        // Hooks to the chart select event for report redrawing
        this.setOptions(opts);
        var chart = this.options.chart;
        if (chart) {
            chart.container.observe('flotr:select', function(evt) {
                var area = evt.memo[0];
                var i1 = gpsData.indexOf(area.x1, _this.options.chart.options.x);
                var i2 = gpsData.indexOf(area.x2, _this.options.chart.options.x);
                _this.draw(i1, i2);
            });
        }
        this.draw();

        return this;
    };

    /**
     * Create a GPS.Planner object
     * @param {string} idptable Id of a HTML table element
     * @param {GPS.Data} gpsData Source of data
     * @param {Object} opts User defined options. See {@link GPS.Planner#options}
     * @class Class to show a table of selected points extracted from a source
     * (the waypoints, routes or tracks). It can calculate also a track GPS.Summary
	 * object for each point.
     */
    GPS.Planner = function(idtable, gpsData, opts) {
        /** DOM node than contains the table. See {@link GPS.Planner#draw}
         * @type node */
        this.container = $(idtable);
        /** 
         * User defined options.
         * Default values: 
         * <pre>{
         *     decimals: {lat: 6, lon: 6, lng: 6, elev: 0, dist: 2,  dist3d: 2, vel: 1, vel3d: 1,
         *              totDist: 2, 'totDist3d': 2, maxElev: 0, minElev: 0, 
         *              elevGain: 0, elevLoss: 0, avgVel: 2, maxVel: 2, totTime: 2, 
         *              totTimeStop: 2, totTimeUp: 2, totTimeFlat: 2, totTimeDown: 2, 
         *              totTimeMove: 2},
         *     source: 'wpt', 
		 *     sourceRef: 1,
		 *     includeStart: true,
         *     includeEnd: true
         * }</pre>
         * Properties:
         * <pre>{object} decimals - A list of number of fixed decimals for each available property
         * {string} source: 'trk', 'wpt' or 'rte'
         * {uint | [uint]} sourceref: When source is 'trk', it could be a number indicating a 
         *     fixed interval of Kilometers to extract points from the track, or an array of numbers 
         *     indicating the exact distances. When source is 'rte' or 'wpt', it could be a number
         *     indicating the step to extract points from the first one, or an array of indexs.</pre>
         * {boolean} includeStart: When source is 'trk', selects if start point it's included or not.
         * {boolean} includeEnd: When source is 'trk', selects if end point it's included or not.
         * You can use also any summary option (see {@link GPS.Summary#options})
         * @type Object
         */
        this.options = {
            decimals: {lat: 6, lon: 6, lng: 6, elev: 0, dist: 2, 
                dist3d: 2, vel: 1, vel3d: 1,
                totDist: 2, 'totDist3d': 2, maxElev: 0, minElev: 0, 
                elevGain: 0, elevLoss: 0, avgVel: 2, maxVel: 2, totTime: 2, 
                totTimeStop: 2, totTimeUp: 2, totTimeFlat: 2, totTimeDown: 2, 
                totTimeMove: 2},
            source: 'wpt',
            sourceRef: 1,
            includeStart: true,
            includeEnd: true
        };
        
		var points = [];
		var summaries = [];
        var hasSummaries = false;

        /**
         * Adds user values to default options replacing existing ones.
         * @param {Object} opts User options 
         * @see GPS.Map#options
         */
        this.setOptions = function(opts) {
            _setOptions(opts, this.options);
        };

		/**
		 * Extract the selected points to be showed in the table.
         * @see GPS.Map#options
         */
		this.loadData = function() {
            var r = this.options.sourceRef;
			var l = 0; 
			var i = 0;
			var j = 0;
            var k = 0;
            function inck() {
				if (Object.isArray(r)) {
				    k = r[l];
				    l++;
				}
				else {
				    k += r;
				}
            }
            if (this.options.includeStart) {
                inck();
            }
			// if source is routes
            if (this.options.source.charAt(0).toUpperCase() == 'R') {
				var m = 0;
				for (i=0; i < gpsData.numOfRoutes; i++) {
					for (j=0; j < gpsData.routepoints[i].length; j++) {
						if (m >= k) {
							points.push(gpsData.routepoints[i][j]);
							inck();
						}
						m++;
					}
				}
			}
			// if source is waypoints
            else if (this.options.source.charAt(0).toUpperCase() == 'W') {
				for (i=0; i < gpsData.numOfWaypoints; i++) {
					if (i >= k) {
	                	points.push(gpsData.waypoints[i]);
	                	inck();
					}
				}
			}
			// if source is traks
            else {
                for (i=0; i < gpsData.numOfTrackpoints; i++) {
                    var p = gpsData.trackpointAt(i);
                    if (p.dist >= k) {
                        points.push(p);
                        inck();
                    }
                }
                if (this.options.includeEnd) {
	                points.push(gpsData.trackpointAt(gpsData.numOfTrackpoints - 1));
                }
            }
		};

		/**
		  * If this.container table contains a th column with a id refered to a GPS.Summary property, 
		  * calculates a Summary for each point.  It needs loadData to be loaded first.
		  */
        this.loadSummaries = function() {
            // Determines if the head of the table contains any Summary property tag.
            var summarytags = "#totdist#totdist3d#minelev#maxelev#elevgain#elevloss#maxvel" +
                    "#avgvel#tottime#tottimestop#tottimeup#tottimedown#tottimeflat#totTimeMove#";
            var head = this.container.getElementsByTagName('th');
            var i = 0;
            for (i=0; i < head.length; i++) {
                if (summarytags.search("#"+head[i].id.toLowerCase()+"#") > 0) {
                    hasSummaries = true;
                    break;
                }
            }
            if (hasSummaries) {
                var j = 0;
                for (i=0; i < points.length; i++) {
                    summaries.push(new GPS.Summary(gpsData, this.options));
                    var from = gpsData.indexOf(points[j].dist, 'dist');
                    var to = gpsData.indexOf(points[i].dist, 'dist');
                    summaries[i].calculate(from, to);
                    j = i;
                }
            }
        };
        
        /**
         * Dumps points values in the table this.container. The table must have
         * at least a row with a &lt;th&gt; element for each desired point property 
         * identified by it DOM id. See {@link GPS.Point}. There is also available 
         * the id 'num' for the correlative order of the point. Existing tbody rows are
         * used as a model for the next ones that its need to fill the table.<br/>
         * Example:
         * @example
         * &lt;table id="planner"&gt;
         * &lt;tr&gt;
         *     &lt;th id='num'&gt;Number&lt;/th&gt;
         *     &lt;th id='dist'&gt;Distance&lt;/th&gt;
         *     &lt;th id='elev'&gt;Elevation&lt;/th&gt;
         * &lt;tr&gt;
         * &lt;/table&gt;
         * @param {param: value} opts User defined options (see {@link GPS.Planner#options})
         */
        this.draw = function() {
            // get column names and void row model
            var head = this.container.getElementsByTagName('th');
            var tBody = this.container.tBodies[0];
            var ids = [];
            var i = 0;
            var lrow = document.createElement('tr');
            for (i=0; i < head.length; i++) {
                ids.push(head[i].id);
            }
            var decimals = this.options.decimals;
			var rows = this.container.tBodies[0].rows;
            var inrows = rows.length;
            var k = 0;
            // for each point
            for (i=0; i < points.length; i++) {
                var point = points[i];
                // if there isn't enough rows inserts one.
                var row = (i < rows.length) ? rows[i] : tBody.appendChild(lrow);
                var cells = row.cells;
                for (var j=0; j < head.length; j++) { // for each cell
                    // if there isn't enough cells  inserts one.
                    if (cells.length < head.length) {
                        row.insertCell(-1);
                    }
                    var id = ids[j];
                    var val = null;
                    if (id == "num") {
                        val = i+1;
                    }
                    else if (point[id] !== undefined) {
                        val = point[id];
                    }
                    else if (hasSummaries) {
                        var summary = summaries[i];
                        val = (summary[id] !== undefined) ? summary[id] : null;
                    }
                    cells[j].innerHTML = "";
                    if (val !== null) {
                        if (id === 'hour') {
                            val = _hourToString(val);
                        }
                        else if (id == 'time') {
                            val = val.toLocaleString();
                        }
                        else {
                            val = _formatNumber(val, decimals[id]);
                        }
                        cells[j].innerHTML = val;
                    }
                }
                if (i+1 >= inrows) {
                    lrow = tBody.rows[k].cloneNode(true);  // use this row as model for the next
                    k++;
                }
            }
        };
        
        // Constructor
        this.setOptions(opts);
        this.loadData();
        this.loadSummaries();
        this.draw();

        return this;
    };

    /** 
     * Create a GPS.Button object
     * @class Class to control a button.
     * The value and alt properties define the behavoir of the button. 
     * There are three kinds:
     * - One click. The usual image (its src file name ends with '_off') is alternated with 
     * a highlitted one (ends with '_on') when the mouse is over. It regret 
     * to normal estate on mouse out. Omit the 'value' and 'alt' properties.
     * - Two states. Change between two diferent images (one called 'value' and 
     * other called 'alt') when cliked. They are highlitted also
     * (ends with '_on' / '_off) on mouse over. Example xaxis and yaxis buttons
     * - On/off. After clicked, the button keeps highlitted when mouse is out until next click.
     * Use value=false and alt=true. Example: zoom button.
     */
    GPS.Button = function(value, alt, onClick) {
        /** Called when on click event with two params: evt (see <a href='http://www.prototypejs.org/api/event'>Event</a> 
         * object and this button object with access to the value and alt properties. See code for examples.
         * @type function */
		this.onClick = onClick;
        /** Normal state value 
         * @type Object */
		this.value = value;
        /** Alternative state value 
         * @type Object */
		this.alt = alt;
    };
	
    /**
     * Create a GPS.ButtonBar object
     * @param {string} idbuttons Id of the button bar DOM element
     * @param {Object} opts User defined options. See {@link GPS.ButtonBar#options}
     * @class Class to control a profile button bar
     */
    GPS.ButtonBar = function(idbuttons, opts) {
        /** DOM node than contains the buttonbar. See {@link GPS.ButtonBar#draw}
         * @type node */
        this.container = $(idbuttons);
        /** 
         * User defined options.
         * Default values: 
         * <pre>{
         *    zoom: {value: false, alt: true,
         *           onClick: // Enable or disable the profile zoom 
         *        }
         *    }, 
         *    xaxis: {value: 'dist', alt: 'hour',
         *            onClick: // Change the data showed in x axis
         *        }
         *    }, 
         *    yaxis: {value: 'elev', alt: 'vel',
         *            onClick: // Change the data showed in y axis
         *        }
         *    }
         * }</pre>
         * Properties:
         * <pre{GPS.Profile} chart - A GPS.Profile object target of the buttons actions
         * {GPS.Report} report - A GPS.Report object target of the zoom button action
         * {GPS.Button} buttonId1
         *   ...
         * {GPS.Button} buttonIdN - You can define as many buttons as needed.
         * - 'buttonId' is ussed to locate the button and must coincide with 
         * the id of its &lt;img&gt; element (see {@link GPS.ButtonBar#draw}). 
         * It's used also to locate the value for the title and alt &lt;img&gt;
         * tags in the i18n settings. See {@link GPS.setI18N}
         */
        this.options = {
            zoom: {  // zoom tool
                value: false, alt: true,
                /** @private */
                onClick: function(evt, button) {
                    if (button.value) {
                        buttonOff(evt);
                        chart.setOptions({zoomEnabled: false});
                        if (report) {
                            report.draw();
                        }
                    }
                    else {
                        chart.setOptions({zoomEnabled: true});
                        buttonOn(evt);
                    }
                    chart.draw();
                }
            }, 
            xaxis: {  // change x axis data
                value: 'dist', alt: 'hour',
                /** @private */
                onClick: function(evt, button) {
                    var xd = (button.value == 'dist' ? 'hour' : 'dist');
                    chart.setOptions({x: xd});
                    chart.loadData();
                    if (report && chart.options.zoomEnabled) {
                        report.draw();
                    }
                    chart.draw();
                }
            }, 
            yaxis: {  // change y axis data
                value: 'elev', alt: 'vel',
                /** @private */
                onClick: function(evt, button) {
                    var yd = (button.value == 'elev' ? 'vel' : 'elev');
                       chart.setOptions({y: yd});
                    chart.loadData();
                    if (report && chart.options.zoomEnabled) {
                        report.draw();
                    }
                    chart.draw();
                }
            }
        };
        
        var chart = opts ? opts.chart : null;
        var report = opts ? opts.report : null;
        
        // Highlights a button
        function buttonOn(evt) {
            evt.target.src = evt.target.src.replace(/_off(\..*)*/, '_on$1');
        }
        
        // Revert to normal state a button
        function buttonOff(evt) {
            if (buttons[evt.target.id].value !== true) {
                evt.target.src = evt.target.src.replace(/_on(\..*)*/, '_off$1');
            }
        }
        
        /**
         * Adds user values to default options replacing existing ones.
         * @param {param: value} opts User options 
         * @type Object
         * @see GPS.ButtonBar#options
         */
        this.setOptions = function(opts) {
            _setOptions(opts, this.options);
        };

        /**
         * Sets title and events of buttons contained in this.container and
         * defined like &lt;img&gt; elements with an id (see {@link GPS.ButtonBar#_setOptions})
         * and src tag. Example:
         * @example
         * &lt;div id="buttons"&gt;
         * &lt;img id="zoom" src="img/zoom_off.png" width="22" height="22"&gt;
         * &lt;img id="xaxis" src="img/dist_off.png" width="22" height="22"&gt;
         * &lt;img id="yaxis" src="img/elev_off.png" width="22" height="22"&gt;
         * &lt;/div&gt;
         * @param {param: value} opts User defined options (see {@link GPS.Profile#options})
         */
        this.draw = function() {
            var s = this.container.getElementsByTagName('img');
            for (var i=0; i < s.length; i++) {
                var id = s[i].id;
                if (buttons[id]) {
                    var p = $(s[i].id); // Redundant, but necesary to IE
                    p.title = i18n["tagbutton" + id + buttons[id].value];
                    p.observe('mouseover', buttonOn);
                    p.observe('mouseout', buttonOff);
                    p.observe('click', function(evt) {
                        var button = buttons[evt.target.id];
                        button.onClick(evt, button);
                        if (button.alt !== undefined) {
                            evt.target.src = evt.target.src.replace(button.value+"_", button.alt+"_");
                            var temp = button.alt;
                            button.alt = button.value;
                            button.value = temp;
                        }
                        evt.target.title = i18n["tagbutton" + evt.target.id + button.value || ''];
                        evt.target.alt = evt.target.title;
                    });
                }
            }
        };
        
        // Constructor. 
        var buttons = this.options;
        this.setOptions(opts);
        this.draw();
        return this;
    };
    
    return GPS;

} ();

